{ lib, pkgs }:
let
  inherit (lib) types;
  inherit (types)
    attrsOf
    oneOf
    coercedTo
    str
    bool
    int
    float
    package
    ;
in
{
  javaProperties =
    {
      comment ? "Generated with Nix",
      boolToString ? lib.boolToString,
    }:
    {

      # Design note:
      # A nested representation of inevitably leads to bad UX:
      # 1. keys like "a.b" must be disallowed, or
      #    the addition of options in a freeformType module
      #    become breaking changes
      # 2. adding a value for "a" after "a"."b" was already
      #    defined leads to a somewhat hard to understand
      #    Nix error, because that's not something you can
      #    do with attrset syntax. Workaround: "a"."", but
      #    that's too little too late. Another workaround:
      #    mkMerge [ { a = ...; } { a.b = ...; } ].
      #
      # Choosing a non-nested representation does mean that
      # we sacrifice the ability to override at the (conceptual)
      # hierarchical levels, _if_ an application exhibits those.
      #
      # Some apps just use periods instead of spaces in an odd
      # mix of attempted categorization and natural language,
      # with no meaningful hierarchy.
      #
      # We _can_ choose to support hierarchical config files
      # via nested attrsets, but the module author should
      # make sure that problem (2) does not occur.
      type =
        let
          elemType =
            oneOf [
              # `package` isn't generalized to `path` because path values
              # are ambiguous. Are they host path strings (toString /foo/bar)
              # or should they be added to the store? ("${/foo/bar}")
              # The user must decide.
              (coercedTo package toString str)

              (coercedTo bool boolToString str)
              (coercedTo int toString str)
              (coercedTo float toString str)
            ]
            // {
              description = "string, package, bool, int or float";
            };
        in
        attrsOf elemType;

      generate =
        name: value:
        pkgs.callPackage (
          {
            runCommand,
            jq,
            libiconvReal,
          }:
          runCommand name
            {
              # Requirements
              # ============
              #
              #  1. Strings in Nix carry over to the same
              #     strings in Java => need proper escapes
              #  2. Generate files quickly
              #      - A JVM would have to match the app's
              #        JVM to avoid build closure bloat
              #      - Even then, JVM startup would slow
              #        down config generation.
              #
              #
              # Implementation
              # ==============
              #
              # Escaping has two steps
              #
              # 1. jq
              #    Escape known separators, in order not
              #    to break up the keys and values.
              #    This handles typical whitespace correctly,
              #    but may produce garbage for other control
              #    characters.
              #
              # 2. iconv
              #    Escape >ascii code points to java escapes,
              #    as .properties files are supposed to be
              #    encoded in ISO 8859-1. It's an old format.
              #    UTF-8 behavior may exist in some apps and
              #    libraries, but we can't rely on this in
              #    general.

              preferLocalBuild = true;
              passAsFile = [ "value" ];
              value = builtins.toJSON value;
              nativeBuildInputs = [
                jq
                libiconvReal
              ];

              jqCode =
                let
                  main = ''
                    to_entries
                      | .[]
                      | "\(
                          .key
                          | ${commonEscapes}
                          | gsub(" "; "\\ ")
                          | gsub("="; "\\=")
                        ) = \(
                          .value
                          | ${commonEscapes}
                          | gsub("^ "; "\\ ")
                          | gsub("\\n "; "\n\\ ")
                        )"
                  '';
                  # Most escapes are equal for both keys and values.
                  commonEscapes = ''
                    gsub("\\\\"; "\\\\")
                    | gsub("\\n"; "\\n\\\n")
                    | gsub("#"; "\\#")
                    | gsub("!"; "\\!")
                    | gsub("\\t"; "\\t")
                    | gsub("\r"; "\\r")
                  '';
                in
                main;

              inputEncoding = "UTF-8";

              inherit comment;

            }
            ''
              (
                echo "$comment" | while read -r ln; do echo "# $ln"; done
                echo
                jq -r --arg hash '#' "$jqCode" "$valuePath" \
                  | iconv --from-code "$inputEncoding" --to-code JAVA \
              ) > "$out"
            ''
        ) { };
    };
}
